SpringBoot2.x版本与shiro从零到一完整的整合(前后端分离)

您所在的位置:网站首页 springboot整合cas 前后端分离 SpringBoot2.x版本与shiro从零到一完整的整合(前后端分离)

SpringBoot2.x版本与shiro从零到一完整的整合(前后端分离)

2024-03-11 16:00| 来源: 网络整理| 查看: 265

前几天使用springoot做了一个项目,我在项目中负责登录验证,以及角色权限管理,角色其实也很简单,老师、学生、管理员等。想写一写自己的心得以及一些配置中遇到的坑,绝对的干货。话不多说,开干.第一次写博客,希望大家看了,给一些中肯的建议,大三也不容易啊。

1.搭建springboot

如果使用IDEA直接创建springboot项目 在这里插入图片描述 如果使用eclipse,直接去这个网站下载后导入即可项目搭建路径

pom文件中的相关依赖 这里只列举几个springboot跟shiro整合的 org.apache.shiro shiro-spring 1.4.0 org.crazycake shiro-redis 3.1.0

这里面可能用到一点redis的知识,不会的自己去学习即可

自定义realm

自定义realm类似于我们的数据源,处理从数据库查询出来的账户密码以及权限验证 需要继承AuthorizingRealm类,并实现里面方法

//授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User userNew =(User) principalCollection.getPrimaryPrincipal(); User user = service.findAllUserAndRoleAndPer(userNew.getUsername()); List stringRole=new ArrayList(); List stringPermission=new ArrayList(); for(Role role:user.getRoleList()){ if(role!=null){ stringRole.add(role.getName()); for(Permission permission:role.getPermissionList()){ if(permission!=null){ stringPermission.add(permission.getName()); } } } } SimpleAuthorizationInfo info=new SimpleAuthorizationInfo(); info.addStringPermissions(stringPermission); info.addRoles(stringRole); return info; } //登录验证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String username=(String) authenticationToken.getPrincipal(); User user = service.findByUsernameBasicInfo(username); if(user==null||user.getPassword()==null||"".equals(user.getPassword())){ return null; } return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName()); }

在登录验证中authenticationToken.getPrincipal(),是获取从前台传递过来的用户名,而 User user = service.findByUsernameBasicInfo(username);我自己写的一个方法,根据用户名获取用户信息,如过存在,则将user、password、getName()传递到SimpleAuthenticationInfo里面即可

在授权方法里User userNew =(User) principalCollection.getPrimaryPrincipal();把刚刚登陆成功的那个对象拿出来查询该对象的权限,因为只有你登录成功才能授权,否则登录都失败了,还谈什么授权。根据用户名把roles跟Permissions全都查询出来,存入集合,以待备用。

自定义sessionManager

自定义的sessionManager,用于当你登陆成功,可以获取一串token字符串,每次前台请求过来的时候,带上这段字符串后台验证,判断是否登陆过,是否有该权限或者角色。一般是将token放入请求头header中,需要继承DefaultWebSessionManager

private static final String AUTHORIZATION="token"; @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String sessionId= WebUtils.toHttp(request).getHeader(AUTHORIZATION); if(sessionId!=null){ request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; }else{ return super.getSessionId(request, response); } } 自定义AuthorizationFilter

在配置shiro路径的时候,roles[a,b],通常是某个用户同时有a和b角色才可以,但是实际问题一般是只要含有二者之一即可,所以我们需要重写该类,继承AuthorizationFilter

@Override protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception { Subject subject = getSubject(servletRequest, servletResponse); //获取当前访问路径所需要的角色集合 String[] rolesArray = (String[]) o; //没有角色限制,可以直接访问 if (rolesArray == null || rolesArray.length == 0) { //no roles specified, so nothing to check - allow access. return true; } Set roles = CollectionUtils.asSet(rolesArray); //当前subject是roles 中的任意一个,则有权限访问 for(String role : roles){ if(subject.hasRole(role)){ return true; } } return false; } 跨域配置管理(踩坑踩了好几天)

需要继承UserFilter,重写preHandle方法,因为前台需要有token传递过来,这个请求属于复杂请求,每次发送请求前,首先发送options请求过来"探路",所以我们需要先把放行,还有Access-control-Allow-Origin的值不能设置成* 否则后台报错

@Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResponse = (HttpServletResponse) response; HttpServletRequest httpRequest = (HttpServletRequest) request; if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { setHeader(httpRequest,httpResponse); return true; }else{ setHeader(httpRequest,httpResponse); return true; } } private void setHeader(HttpServletRequest request,HttpServletResponse response){ //跨域的header设置 response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin")); response.setHeader("Access-Control-Allow-Methods", "GET,,POST,PUT,DELETE"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers")); response.setHeader("Content-Type","application/json;charset=UTF-8"); } redis缓存的基本配置 @Component @ConfigurationProperties(prefix = "redis") @PropertySource("classpath:application.properties") @Data public class RedisConfig { private Integer port; private String host; private String password; private Integer database; } 重头戏来了!!!配置最多的类 @Configuration public class ShiroConfiguration { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //设置如果未登录时需要跳转的url shiroFilterFactoryBean.setLoginUrl(""); //设置登陆成功后跳转的界面或者url,如果是前后端分离,则可以不写该选项 shiroFilterFactoryBean.setSuccessUrl(""); //登录成功后,但是未授权,则调到未授权的url shiroFilterFactoryBean.setUnauthorizedUrl(""); //将自定义的角色过滤器放到map里面 Map map = new LinkedHashMap(); //这里配置好角色过滤器时,拦截路径时的前缀要写成key的值 map.put("customRoles", 自定义的AuthorizationFilter); //自定义的过滤器解决跨域 map.put("corsFilter", 自定义的跨域过滤器); //将自定义的角色过滤器放到shiroFilterFactoryBean里面去 shiroFilterFactoryBean.setFilters(map); !!!!注意这里有个坑:一定要使用LinkedHashMap而不是hashmap,因为hashmap是无序的, 对于路径的映射,一旦匹配到就会立即返回,我们的配置也是有顺序的,可能有的小伙伴使用了hashmap,路径有时匹配成功,有时失败 //拦截器路径,同一拦截,注意要使用linkedHashMap保证过滤器的顺序 Map filterMap=new LinkedHashMap(); //key是需要拦截的路径,value采用哪种过滤器,或者那种角色或权限 //跨域拦截 filterMap.put("/**", "corsFilter"); //登出过滤器 filterMap.put("", "logout"); //匿名访问,也就是说无需的登录即可访问 filterMap.put("", "anon"); //需要登录才能访问的 filterMap.put("", "authc"); //有相应角色才能访问的,例如管理员才能访问 filterMap.put("", "customRoles[admin,root]"); //这里对应上面自定义AuthorizationFilter的key //有相应权限才能访问的,例如有 filterMap.put("", "perms[*]"); //全局拦截,避免遗漏哪些路径,放到最下面,这里也是要求必须使用linkedhashmap的理由 filterMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); return shiroFilterFactoryBean; } //安全管理器 @Bean public SecurityManager securityManager(RedisCacheManager redisCacheManager){ DefaultWebSecurityManager manager=new DefaultWebSecurityManager(); //设置会话管理 manager.setSessionManager(customSessionManager()); //设置realm manager.setRealm(customRealm()); //设置缓存 manager.setCacheManager(redisCacheManager); return manager; } //自定义realm @Bean public CustomRealm customRealm(){ CustomRealm customRealm=new CustomRealm(); customRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return customRealm; } //自定义session管理器 @Bean public CustomSessionManager customSessionManager(){ CustomSessionManager customSessionManager=new CustomSessionManager(); customSessionManager.setSessionDAO(redisSessionDAO()); return customSessionManager; } //密码匹配器 @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5"); hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } @Autowired private RedisConfig redisConfig; //缓存管理 @Bean public RedisManager redisManager(){ RedisManager redisManager=new RedisManager(); redisManager.setDatabase(redisConfig.getDatabase()); redisManager.setHost(redisConfig.getHost()); redisManager.setPort(redisConfig.getPort()); redisManager.setPassword(redisConfig.getPassword()); return redisManager; } //cache管理器 @Bean public RedisCacheManager redisCacheManager(RedisManager redisManager){ RedisCacheManager redisCacheManager=new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager); //设置过期时间,单位是秒 redisCacheManager.setExpire(60*30); return redisCacheManager; } //设置sessionDao @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } 登录的controller public Map login(String username,String password){ Map map = new HashMap(); Subject subject= SecurityUtils.getSubject(); UsernamePasswordToken token=new UsernamePasswordToken(username, password); try{ subject.login(token); //获取sessionid,传到前台 Serializable sessionId = subject.getSession().getId(); map.put("message", RbacStatus.SUCCESS_IN.getMessage()); map.put("session_id", sessionId); map.put("code", RbacStatus.SUCCESS_IN.getCode()); return map; }catch (Exception e){ map.clear(); e.printStackTrace(); map.put("message", RbacStatus.ERROR_IN.getMessage()); map.put("code", RbacStatus.ERROR_IN.getCode()); return map; } }

这里使用redis的原因有两种: 1.在权限校验时,每次需要查询数据库,用户是否拥有该角色或者权限,访问频率较高,而且修改少,正好符合使用redis特点,对于登录来说,访问频率低,不建议使用。 2.可以将前台传递的token放入redis中,类似于分布式session共享,假如项目集群部署时,放入redis,也可以达到共享,唯一的缺点需要自己维护过期时间,可以每次请求后台路径的时候,更新一次时间,或者前台使用js做个定时器也是可以的,emmmm就这样吧。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3